package cn.sciento.export;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import cn.sciento.core.exception.CommonException;
import org.apache.commons.codec.Charsets;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import cn.sciento.core.util.FilenameUtils;
import cn.sciento.core.util.Pair;
import cn.sciento.export.util.ShardUtils;
import cn.sciento.export.vo.ExportColumn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class ExcelExporter {
    public static final int MAX_ROW = 1000000;

    private static final String TXT_SUFFIX = ".txt";

    private static final String ZIP_SUFFIX = ".zip";

    private static final String EXCEL_SUFFIX = ".xlsx";

    private static final String CONTENT_TYPE_TXT_FILE = "text/plain";

    private static final String CONTENT_TYPE_ZIP_FILE = "application/zip";

    private static final String CONTENT_TYPE_EXCEL_FILE = "application/vnd.ms-excel";

    private static final int READ_BUFFER_SIZE = 4194304;

    private static final Logger LOGGER = LoggerFactory.getLogger(ExcelExporter.class);

    private List<SXSSFWorkbook> workbooks;

    private ExportColumn root;

    private List<ExcelFiller> excelFillers;

    private String fillerType;

    private int singleExcelMaxRow;

    private int singleSheetMaxRow;

    private int singleExcelMaxSheetNum;

    private File temp;

    private InputStream inputStream;

    private HttpServletRequest request;

    private HttpServletResponse response;

    private String fileName;

    private int pageExportDataIndex;

    private String errorMessage;

    public ExcelExporter(ExportColumn root, String fillerType, int singleExcelMaxSheetNum, int singleSheetMaxRow, HttpServletResponse response) {
        this.workbooks = new ArrayList<>();
        this.excelFillers = new ArrayList<>();
        this.fillerType = fillerType;
        this.singleExcelMaxSheetNum = singleExcelMaxSheetNum;
        this.singleSheetMaxRow = singleSheetMaxRow;
        this.singleExcelMaxRow = singleSheetMaxRow * this.singleExcelMaxSheetNum;
        this.root = root;
        this.request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        this.response = response;
        addShard();
    }

    public ExcelExporter(ExportColumn root, String fillerType, HttpServletResponse response) {
        this(root, fillerType, 5, 1000000, response);
    }

    public ExcelExporter(ExportColumn root, ExcelFiller filler, HttpServletResponse response) {
        this.workbooks = new ArrayList<>();
        this.excelFillers = new ArrayList<>();
        this.fillerType = filler.getFillerType();
        SXSSFWorkbook shard = new SXSSFWorkbook();
        this.workbooks.add(shard);
        this.excelFillers.add(filler);
        this.singleExcelMaxSheetNum = 5;
        this.singleSheetMaxRow = 1000000;
        this.singleExcelMaxRow = 1000000 * this.singleExcelMaxSheetNum;
        this.root = root;
        this.request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        this.response = response;
    }

    private void addShard() {
        SXSSFWorkbook shard = new SXSSFWorkbook();
        this.workbooks.add(shard);
        ExcelFiller excelFiller = ExcelFillerHolder.getExcelFiller(this.fillerType, shard);
        excelFiller.configure(this.singleSheetMaxRow);
        Assert.notNull(excelFiller, "export.filler.not-found");
        this.excelFillers.add(excelFiller);
    }

    private void splitShard(int size) {
        int addSize = size - this.workbooks.size();
        for (int i = 0; i < addSize; i++)
            addShard();
    }

    public void fillSheet(List<?> exportData) {
        if (CollectionUtils.isEmpty(exportData) || exportData.get(0) == null)
            return;
        try {
            fillSheetWithShard(exportData);
        } catch (Exception e) {
            LOGGER.error("fill data occurred error.", e);
        }
    }

    public void fillSheetForPage(List<?> exportData) {
        if (CollectionUtils.isEmpty(exportData) || exportData.get(0) == null)
            return;
        if (exportData.size() > this.singleSheetMaxRow)
            throw new CommonException("The number of single pagination queries should not be greater than the maximum number of rows in a single sheet page(@{pageSize=" + exportData.size() + "} > @{singleSheetMaxRow=" + this.singleSheetMaxRow + "}).Please try to configure a larger maximum number of rows in a single sheet page or reduce the size of a single page.");
        int shardNum = ++this.pageExportDataIndex / this.singleExcelMaxSheetNum + 1;
        splitShard(shardNum);
        doFillData(this.workbooks.get(shardNum - 1), this.root, this.excelFillers.get(shardNum - 1), exportData, true);
    }

    private void fillSheetWithShard(List<?> exportData) {
        int sum = exportData.size();
        Pair<Integer, Integer> pair = ShardUtils.shard(sum, this.singleExcelMaxRow);
        int shardNum = ((Integer) pair.getFirst()).intValue();
        int lastNum = ((Integer) pair.getSecond()).intValue();
        splitShard(shardNum);
        List<List<?>> shardData = ShardUtils.prepareShardData(exportData, shardNum, this.singleExcelMaxRow, lastNum);
        fillDataWithShard(this.workbooks, this.root, this.excelFillers, shardData);
    }

    private void fillDataWithShard(List<SXSSFWorkbook> workbooks, ExportColumn root, List<ExcelFiller> excelFillers, List<List<?>> shardData) {
        if (workbooks.size() > excelFillers.size())
            throw new IllegalStateException("分片异常");
        for (int i = 0; i < workbooks.size(); i++) {
            ExcelFiller excelFiller = excelFillers.get(i);
            SXSSFWorkbook workbook = workbooks.get(i);
            List<?> exportData = shardData.get(i);
            doFillData(workbook, root, excelFiller, exportData, false);
        }
    }

    private void doFillData(SXSSFWorkbook workbook, ExportColumn root, ExcelFiller excelFiller, List<?> exportData, boolean paging) {
        excelFiller.fillDataWithShard(workbook, root, exportData, paging);
    }

    public void fillTitle() {
        SXSSFWorkbook workbook = this.workbooks.get(0);
        if (workbook.getNumberOfSheets() == 0)
            ((ExcelFiller) this.excelFillers.get(0)).createSheetAndTitle(workbook, this.root);
    }

    public void setExcelHeader() {
        String contentType = getContentType();
        String filename = getDownloadFileName();
        String encodeFileName = null;
        try {
            encodeFileName = FilenameUtils.encodeFileName(this.request, filename);
        } catch (IOException e) {
            LOGGER.error("encode file name failed.", e);
        }
        this.response.addHeader("Content-Disposition",
                String.format("attachment;filename=\"%s\"", new Object[]{encodeFileName}));
        this.response.setHeader("Accept-Ranges", "bytes");
        this.response.setContentType(contentType + "; charset=" + Charsets.UTF_8.displayName());
        this.response.setCharacterEncoding(Charsets.UTF_8.displayName());
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFileName() {
        return (this.fileName == null) ? this.root.getTitle() : this.fileName;
    }

    private void writeBytes(ServletOutputStream outputStream) throws IOException {
        try {
            if (!StringUtils.isEmpty(this.errorMessage)) {
                outputStream.write(this.errorMessage.getBytes());
                return;
            }
            if (this.workbooks.size() == 1) {
                ((SXSSFWorkbook) this.workbooks.get(0)).write((OutputStream) outputStream);
            } else {
                writeZipBytes((OutputStream) outputStream);
            }
        } catch (IOException e) {
            LOGGER.error("IO exception when write response", e);
        } finally {
            if (outputStream != null)
                outputStream.close();
        }
    }

    private void writeOutputStream(InputStream inputStream, ServletOutputStream outputStream) throws IOException {
        byte[] buffer = new byte[4194304];
        int read;
        while ((read = inputStream.read(buffer, 0, 4194304)) != -1)
            outputStream.write(buffer, 0, read);
        outputStream.flush();
    }

    private void writeZipBytes(OutputStream outputStream) throws IOException {
        ZipOutputStream zipOutputStream = null;
        try {
            zipOutputStream = new ZipOutputStream(outputStream);
            ZipEntry zipEntry = null;
            for (int i = 0; i < this.workbooks.size(); i++) {
                SXSSFWorkbook workbook = this.workbooks.get(i);
                zipEntry = new ZipEntry(this.root.getTitle() + "-" + (i + 1) + ".xlsx");
                zipOutputStream.putNextEntry(zipEntry);
                workbook.write(zipOutputStream);
            }
            zipOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (zipOutputStream != null) {
                zipOutputStream.closeEntry();
                zipOutputStream.close();
            }
        }
    }

    private File buildTempFile() throws IOException {
        File tmp = null;
        FileOutputStream fos = null;
        try {
            if (this.workbooks.size() == 1) {
                tmp = new File(UUID.randomUUID() + ".xlsx");
                boolean create = tmp.createNewFile();
                Assert.isTrue(create, "create tmp file error!");
                fos = new FileOutputStream(tmp);
                ((SXSSFWorkbook) this.workbooks.get(0)).write(fos);
                fos.flush();
            } else {
                tmp = new File(UUID.randomUUID() + ".zip");
                boolean create = tmp.createNewFile();
                Assert.isTrue(create, "create tmp file error!");
                fos = new FileOutputStream(tmp);
                writeZipBytes(fos);
                fos.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null)
                fos.close();
        }
        return tmp;
    }

    private byte[] readDataFromTempFile(File tmp) throws IOException {
        FileInputStream fis = null;
        byte[] buffer = null;
        try {
            fis = new FileInputStream(tmp);
            int max = fis.available();
            buffer = new byte[max];
            fis.read(buffer, 0, max);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null)
                fis.close();
        }
        return buffer;
    }

    private InputStream readInputStreamFromTempFile(File temp) throws IOException {
        if (this.inputStream == null)
            this.inputStream = new FileInputStream(temp);
        return this.inputStream;
    }

    public void downloadExcel() throws IOException {
        setExcelHeader();
        writeBytes(this.response.getOutputStream());
    }

    public byte[] readFileBytes() throws IOException {
        if (this.temp == null)
            this.temp = buildTempFile();
        return readDataFromTempFile(this.temp);
    }

    public InputStream readInputStream() throws IOException {
        if (this.temp == null)
            this.temp = buildTempFile();
        return readInputStreamFromTempFile(this.temp);
    }

    public void cleanTempFile() throws IOException {
        if (this.inputStream != null)
            this.inputStream.close();
        if (this.temp != null) {
            boolean del = this.temp.delete();
            Assert.isTrue(del, "delete tmp file error!");
        }
    }

    public String getDownloadFileName() {
        String suffix = getFileSuffix();
        return ".txt".equals(suffix) ? ("error" + suffix) : (getFileName() + suffix);
    }

    public String getFileSuffix() {
        if (!StringUtils.isEmpty(this.errorMessage))
            return ".txt";
        return isZip() ? ".zip" : ".xlsx";
    }

    public String getContentType() {
        if (!StringUtils.isEmpty(this.errorMessage))
            return "text/plain";
        return isZip() ? "application/zip" : "application/vnd.ms-excel";
    }

    public boolean isZip() {
        return (this.workbooks.size() > 1);
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }
}
